Skip to content

fix(log_object_drops): correct commit LSN, DROP DATABASE path, TEMP skip, and PITR docs (v5 review fixes)#55

Open
NikolayS wants to merge 1 commit into
masterfrom
fix/drop-lsn-v5-fixes
Open

fix(log_object_drops): correct commit LSN, DROP DATABASE path, TEMP skip, and PITR docs (v5 review fixes)#55
NikolayS wants to merge 1 commit into
masterfrom
fix/drop-lsn-v5-fixes

Conversation

@NikolayS

Copy link
Copy Markdown
Owner

Fixes the four defects Kirk Wolak raised against v5 of "PoC: Simplify recovery after dropping a table by LOGGING the restore LSN", plus a fifth found while writing the PITR test. Andrey Borodin confirmed on the Hacking Postgres call that Kirk's list is "a real list of problems in the patch".

Targets the pgsql-hackers thread and CF #6272.

Base: v5 attachment v5-0001-Add-log_object_drops-GUC-for-DROP-TABLE-DATABASE-.patch (authored by Dmitry Lebedev, rebased and posted by Andrey Borodin 2025-12-10 — note the thread's v5 is Andrey's post, not Dmitry's), applied on origin/master @ 11ed011ae22.

Rebase notes — v5 does not build on current master

The CF entry's CI has been red for months. Two independent reasons, both fixed here:

  1. guc_parameters.dat now enforces alphabetical ordering. v5 appends log_object_drops after zero_damaged_pages, which fails the build outright:
    guc_parameters.dat:3676: error: entries are not in alphabetical order: "zero_damaged_pages", "log_object_drops"
    
    Moved between log_min_messages and log_parameter_max_length.
  2. Test file number collision. t/050_drop_table_logging.pl collides with master's existing 050_redo_segment_missing.pl (master is up to 054_*). Renumbered to 055, which is the only meson.build conflict.

The four fixes

1. Format-string bug — confirmed as real UB, not cosmetic

The format had four %X but passed one LSN_FORMAT_ARGS(), which expands to two arguments:

errmsg("DROP TABLE: relation \"%s.%s\" (OID %u), "
       "drop LSN: %X/%X, commit LSN: %X/%X",
       info->schemaname, info->relname, info->reloid,
       LSN_FORMAT_ARGS(commit_lsn))   /* -> 2 args, not 4 */

Verified with a standalone repro using the same __attribute__((format(printf,...))) that errmsg carries:

warning: more '%' conversions than data arguments [-Wformat-insufficient-args]
    "drop LSN: %X/%X, commit LSN: %X/%X",
                                  ~^

The trailing two %X read past the varargs and print stack garbage. No "drop LSN" is recorded anywhere in the patch, and it would not be useful for PITR if it were, so the message now reports only the commit LSN. Also switched %X/%X to master's %X/%08X convention (708 uses vs 26 legacy), which makes the logged value directly comparable to pg_lsn output.

2. DROP DATABASE still used GetXLogInsertRecPtr()

Kirill Reshke's report, unfixed on that path. The value is read before the commit record is written, and dropdb() then forces an immediate checkpoint (RequestCheckpoint(CHECKPOINT_FAST | CHECKPOINT_FORCE | CHECKPOINT_WAIT)) before committing — so the logged LSN could be far behind the real commit LSN and was not a valid recovery target.

DROP DATABASE now uses the same deferred XactCallback path as tables. The per-transaction machinery in dependency.c is generalized from tables-only to DropObjectInfo { kind, oid, name, schema }, with RegisterDropDatabaseForLogging() exported for dbcommands.c (which does not go through performDeletion()).

Side effect worth noting: a DROP DATABASE that errors out after the catalog delete is no longer logged. The old code logged unconditionally, before the drop was durable.

3. Skip TEMP

Temp tables are excluded via get_rel_persistence() != RELPERSISTENCE_TEMP. They are not PITR-recoverable and are dropped at every session exit, so the entries were pure noise. Test 9 of v5 asserted the opposite (expected temp drops to be logged); it is inverted and expanded to also cover ON COMMIT DROP and implicit session-exit cleanup.

4. Document the PITR use, including the recovery_target_inclusive trap

The docs had no mention of PITR at all. Added a paragraph making the correct incantation unmissable — recovery_target_inclusive defaults to on, which stops recovery after the target, so a DBA setting only recovery_target_lsn replays the drop and loses the table again:

recovery_target_lsn = '0/015D4A48'   # the commit LSN from the log message
recovery_target_inclusive = off      # required: stop BEFORE the drop commits

5. Bonus defect found while writing the PITR test

The documented recipe in (4) did not actually work, because v5 logs the wrong end of the commit record.

XactLogCommitRecord() returns XLogInsert()'s EndRecPtr — the end of the commit record. But recovery_target_lsn is compared against each record's start:

/* recoveryStopsBefore(), xlogrecovery.c */
if (recoveryTarget == RECOVERY_TARGET_LSN &&
    !recoveryTargetInclusive &&
    record->ReadRecPtr >= recoveryTargetLSN)   /* ReadRecPtr = record START */

The end LSN of the commit record is the start of the following record, so recovery with recovery_target_inclusive = off stops before the record after the commit — i.e. it still replays the drop. The exact failure the feature exists to prevent, silently, even for a DBA who follows the docs correctly.

RecordTransactionCommit() now reports ProcLastRecPtr, the start of the commit record just inserted — the same idiom xlog.c uses for checkpoints ("We now have ProcLastRecPtr = start of actual checkpoint record"). Confirmed with pg_waldump, the logged LSN lands exactly on the drop transaction's commit record:

rmgr: Transaction len (rec/tot): 473/473, tx: 780, lsn: 0/0401ABD0, prev 0/0401AB98,
      desc: COMMIT ...; rels: base/5/16537; dropped stats: 2/5/16537; ...

Without this fix the PITR test below fails outright. This is the item I would most like reviewed on-list — it changes what commit_lsn means in the XactCallback API v5 introduces.

Testing

Andrey explicitly asked for better testing. v5's test largely asserted "a line exists", which cannot distinguish a correct LSN from garbage. t/055_drop_table_logging.pl is rewritten: 54 assertions, all passing.

The key one writes ~7 MB of WAL between the DROP and the COMMIT, so the drop LSN and the commit LSN cannot be confused — a GetXLogInsertRecPtr()-at-drop-time implementation fails it by a wide margin:

ok 1 - Test 1: exactly one entry logged
# logged=0/01EFDD98 after_drop=0/01754F80 before_commit=0/01EFDD98 after_commit=0/01EFDF78
ok 3 - Test 1: logged LSN is past the drop LSN (not GetXLogInsertRecPtr at drop time)
ok 4 - Test 1: logged LSN is at or after the start of the commit record
ok 5 - Test 1: logged LSN is the START of the commit record, not its end

logged == before_commit exactly, and sits ~7.6 MB past the drop LSN. DROP DATABASE is pinned the same way (# db logged=0/0279DC78 before=0/0279DA68 after=0/0279DCE0).

End-to-end PITR — restores a real basebackup to the logged LSN and proves both directions of the trap:

# PITR target LSN = 0/0401B1D0
ok 52 - Test 23a: recovery_target_inclusive=off recovers the dropped table
ok 53 - Test 23a: recovered table has its rows
ok 54 - Test 23b: default recovery_target_inclusive=on replays the drop (documented trap)

Also covered: nothing logged when the GUC is off (with an on/off control pair), on ROLLBACK, on ROLLBACK TO SAVEPOINT, for TEMP (explicit / ON COMMIT DROP / session exit), for a TEMP sibling without suppressing a permanent table dropped in the same transaction, for views/indexes, and for a failed DROP DATABASE; plus COMMIT AND CHAIN, ROLLBACK AND CHAIN, savepoints, partitions, inheritance, DROP SCHEMA CASCADE, and PL/pgSQL EXCEPTION subtransactions (both committed and rolled back).

1/1 recovery - postgresql:recovery/055_drop_table_logging OK  3.91s  54 subtests passed

Regression runs on this branch (macOS/arm64, meson, --with-cassert):

postgresql:regress/regress          OK   36.12s   245 subtests passed
postgresql:plpgsql/regress          OK    2.70s    13 subtests passed
recovery suite:  Ok: 48   Fail: 0   Skipped: 6

What I could NOT verify

Stated plainly:

  • Docs do not build in this environment. ninja alldocs fails fetching the DocBook 4.5 DTD over the network (Attempt to load network entity http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd) — a local toolchain gap, unrelated to this change. I verified the new <varlistentry> parses as well-formed XML and that both <xref linkend=...> targets (guc-recovery-target-inclusive, guc-recovery-target-lsn) exist in config.sgml. The rendered output is unverified.
  • pgindent not runpg_bsd_indent is not installed here. Indentation was matched by hand and git diff --check is clean, but this has not been through the real formatter and may drift.
  • Single platform only — macOS/arm64, clang, meson, cassert. No Linux, no 32-bit, no autoconf/./configure build, no Windows, no CI.
  • sepgsql not compiled (selinux: NO in this build), so its one-line XactCallback signature change is untested beyond inspection. Same for postgres_fdw's callback change: the file compiles, but I did not run the postgres_fdw suite.
  • The LSN exactness assertions assume a quiet WAL stream. The test sets autovacuum = off, checkpoint_timeout = 1h, max_wal_size = 1GB to that end, and the start-of-commit-record bound is asserted as >= rather than == precisely because a commit record's start can be nudged past a WAL page header. Still, these are the assertions most likely to prove flaky on a loaded buildfarm animal, and that risk is real.
  • Not posted to pgsql-hackers. This is a GitHub PR only; it has not been sent to the thread or attached to the CF entry.

Open questions for the list

  • Should unlogged tables be skipped too? Their contents are not PITR-recoverable either, though unlike temp tables the definition is. Kirk's item said TEMP only, so I did not expand scope.
  • Fix 5 changes the meaning of the LSN passed to XactCallback from end-of-commit-record to start. If that callback is intended as general infrastructure rather than something private to this feature, end-of-record may be the more conventional choice, and this feature should derive the start itself — but then the GUC must not hand a DBA an LSN that silently replays the drop.
  • The Assert(!XLogRecPtrIsInvalid(commit_lsn)) inherited from v5 is kept. It holds because any transaction dropping a table or database necessarily has an XID and writes a commit record, but it is an assert in a callback that fires on every commit.

🤖 Generated with Claude Code

…and PITR docs

Rebase of v5 ("Add log_object_drops GUC for DROP TABLE/DATABASE logging",
Dmitry Lebedev, rebased by Andrey Borodin) onto current master, plus fixes
for the four defects Kirk Wolak raised, and a fifth found while testing.

Rebase (v5 does not build on master as posted):
* guc_parameters.dat now enforces alphabetical ordering; log_object_drops was
  appended after zero_damaged_pages, which fails the build outright.
* t/050_drop_table_logging.pl collided with the existing 050_redo_segment_missing.pl;
  renumbered to 055.

1. Format-string bug (real UB, not cosmetic). The errmsg format had four %X
   conversions ("drop LSN: %X/%X, commit LSN: %X/%X") but passed a single
   LSN_FORMAT_ARGS(), which expands to two arguments. clang reports
   -Wformat-insufficient-args; the trailing two %X read past the varargs and
   print stack garbage. There is no "drop LSN" recorded anywhere, and it would
   not be useful for PITR if there were, so the message now reports only the
   commit LSN. Also switched %X/%X to master's %X/%08X convention, which makes
   the logged value directly comparable to pg_lsn output.

2. DROP DATABASE used GetXLogInsertRecPtr() (Kirill Reshke's report). That is
   read before the commit record is written, and dropdb() still forces an
   immediate checkpoint afterwards, so the value could be far behind the actual
   commit LSN and was not a valid recovery target. DROP DATABASE now goes
   through the same deferred XactCallback path as tables. As a side effect, a
   DROP DATABASE that errors out after the catalog delete is no longer logged.

3. Temporary tables are no longer logged. Their contents are not recoverable by
   PITR, and they are dropped at every session exit, so the entries were noise.

4. Documented the PITR recipe, including the recovery_target_inclusive trap:
   the default of on stops recovery *after* the target, so setting only
   recovery_target_lsn replays the drop and loses the object again.

5. (Found while writing the PITR test.) The logged LSN was the *end* of the
   commit record, since XactLogCommitRecord() returns XLogInsert()'s EndRecPtr.
   recovery_target_lsn is compared against each record's *start*
   (recoveryStopsBefore() tests record->ReadRecPtr), so the end LSN is the start
   of the following record: recovery with recovery_target_inclusive=off would
   still replay the drop. RecordTransactionCommit() now reports ProcLastRecPtr,
   the start of the commit record, which is what a DBA can feed back to
   recovery_target_lsn. Without this the documented recipe in (4) does not work,
   and the TAP test in this commit fails.

Testing (src/test/recovery/t/055_drop_table_logging.pl, 54 assertions):
* the logged LSN is the actual commit LSN, verified against
  pg_current_wal_insert_lsn() and by writing ~7MB of WAL between the DROP and
  the COMMIT so that the drop LSN and commit LSN cannot be confused;
* nothing is logged when the GUC is off, on ROLLBACK, on ROLLBACK TO SAVEPOINT,
  for TEMP tables (explicit, ON COMMIT DROP, and session-exit), or when
  DROP DATABASE fails;
* an end-to-end PITR test restores a backup to the logged LSN and asserts the
  dropped table and its rows come back with recovery_target_inclusive=off, and
  that the default of on loses them.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
@NikolayS

Copy link
Copy Markdown
Owner Author

Tracking issue: #56

@NikolayS

Copy link
Copy Markdown
Owner Author

The LSN correctness bug (was #53)

Summary

The log_object_drops patch (v5) logs the end LSN of the commit record. All recovery_target_lsn comparisons in recovery are made against the record's start LSN. Therefore the logged LSN can never be used to stop before the drop, with any value of recovery_target_inclusive.

This is a design-level correctness bug, not a documentation gap. The feature as currently shipped does not do the thing it exists to do.

Evidence (verified against master, 2026-07-15)

What v5 logsXactLogCommitRecord() returns the result of XLogInsert(), which is the EndPos (the position after the record):

/* src/backend/access/transam/xact.c */
commit_lsn = XactLogCommitRecord(GetCurrentTransactionStopTimestamp(),
                    nchildren, children, nrels, rels,
                    ...

What recovery comparesrecoveryStopsBefore() tests the record's start (ReadRecPtr):

/* src/backend/access/transam/xlogrecovery.c:2580-2582 */
if (recoveryTarget == RECOVERY_TARGET_LSN &&
    !recoveryTargetInclusive &&
    record->ReadRecPtr >= recoveryTargetLSN)

recoveryStopsAfter() compares against record start as well (xlogrecovery.c:2748).

Why this breaks

Let the DROP's commit record span [start, end). v5 logs end.

  • recovery_target_lsn = end, recovery_target_inclusive = off: the commit record's own ReadRecPtr is start, and start < end, so the stop-before test does not fire on it — the commit record is replayed (the table is dropped). The test then fires on the next record, whose start is >= end. Net effect: recovery stops after the drop.
  • recovery_target_inclusive = on: stops after the target. Also after the drop.

Either way the table is gone. There is no setting of recovery_target_inclusive that recovers the table.

Fix options

  1. Log ProcLastRecPtr instead — the start of the last record inserted by this backend (extern PGDLLIMPORT XLogRecPtr ProcLastRecPtr;, src/include/access/xlog.h:33). With a start LSN, recovery_target_lsn = <start> + recovery_target_inclusive = off correctly stops before the commit record.
  2. Log the XID as well, and recommend recovery_target_xid = <xid> with recovery_target_inclusive = off. This sidesteps LSN start/end arithmetic entirely and is arguably the friendlier DBA-facing recipe.

Recommend doing both: the XID recipe is what a human should copy-paste; the correct start LSN is what tooling should consume.

Relationship to the other known issues

Kirk Wolak's list from the 2026-07-15 session:

  1. Format-string bug (errmsg wants 4x %X but one LSN_FORMAT_ARGS is passed) — unaffected, fix as-is.
  2. DROP DATABASE still uses GetXLogInsertRecPtr() — the deferral to commit time is right, but the deferred value must be the record start, not XactLogCommitRecord()'s return value.
  3. Skip TEMP — unaffected.
  4. "Document PITR use (recovery_target_inclusive)" — insufficient on its own. Documenting the incantation does not help when no value of the setting produces the advertised behaviour. Item 4 was a symptom of this bug.

Test implication

A TAP test that asserts "an LSN was logged" cannot catch this. The test must assert the logged LSN actually restores the dropped table — i.e. run a real PITR to the logged target and check the table is present. Anything weaker passes against a broken feature.

Refs


🤖 Generated with Claude Code

@NikolayS

Copy link
Copy Markdown
Owner Author

Design review: generalizing log_object_drops (was #54)

Design review of the log_object_drops / restore-LSN-on-DROP feature, produced live during the Hacking Postgres session of 2026-07-15 (Kirk Wolak, Andrey Borodin, Nikolay Samokhvalov).

Two agents ran, answering two different framings of Andrey's question: "can we spend the creativity budget on generalizing the feature — why is it needed, why is it valuable, how can this be done more in a Postgres way?"

All code claims below were checked against master as of 2026-07-15. The correctness bug they surfaced is filed separately as #53read that one first, it is the load-bearing finding.


Memo 1 — the general question (extension allowed)

TL;DR: Everything this patch needs already exists in core: OAT_DROP fires for tables and databases, XactLastCommitEnd is an exported global set before XACT_EVENT_COMMIT callbacks run, and XLogRestorePoint() is an exported function. The feature could be an extension emitting a named restore point pre-commit plus a log line announcing it — and the only core-worthy piece is documenting/stabilizing the commit-LSN-in-callback contract.

Verified facts

Claim Location
OAT_DROP fires for tables dependency.c:1320
OAT_DROP fires for databases (event triggers do not cover DROP DATABASE) dbcommands.c:1741
XactLastCommitEnd is PGDLLIMPORT xlog.h:35
...assigned in RecordTransactionCommit before CallXactCallbacks(XACT_EVENT_COMMIT) xact.c:1602 vs xact.c:2449
...but skipped when a xact writes no WAL (stale-value trap) xact.c:1603
XLogRestorePoint() exported xlog.h:274
RECOVERY_TARGET_NAME stops after the first matching restore point xlogrecovery.c:2724-2741
XACT_EVENT_PRE_COMMIT is outside the commit critical section xact.c:2322

Key points

Kirill Reshke's unanswered question. In March 2025 he asked on the thread: "What stops us from logging all the same inside object access hook defined by extension?" Nobody replied. The answer is: nothing. An extension can hook OAT_DROP, stash names, track subxact aborts via RegisterSubXactCallback, write XLogRestorePoint() in PRE_COMMIT, and log XactLastCommitEnd at COMMIT — roughly 200 lines, zero core changes. This objection will be raised again by a committer; the thread needs an answer ready.

Restore points vs. log lines — not either/or. A log line lives on the machine you may have just destroyed; a restore point is a WAL record — archived, replicated, readable by pg_waldump with zero logs. recovery_target_name also has no inclusive/exclusive footgun. Because RECOVERY_TARGET_NAME stops after the first match, the restore point must be written at PRE_COMMIT, and names need an LSN/xid uniquifier first (a twice-dropped-and-recreated table must not send you to the wrong epoch). Recommendation: restore point as the durable artifact, log line as the discovery channel.

Orthogonality. The feature is a composition of three facilities that already exist (drop-event surface, commit-LSN exposure, recovery landmarks). That composition is the Postgres way — and is the argument for it living outside core. A general "record landmark on DDL class" core mechanism is architecture-astronomy.

Docs. Primary home is the Continuous Archiving / PITR chapter, cross-linked from the DROP TABLE reference page — the page a panicking DBA actually opens. Burying it in runtime-config-logging hides it from the only person who needs it.


Memo 2 — the same question with the extension option removed

Per Andrey on the call: "maybe we can ask it to consider again if we don't want anything like an extension", and Kirk: "we try to get away with it in the core, and only push it into a contrib if we're forced to by hackers."

TL;DR: In-core changes the answer — drop the restore-point machinery entirely. Core can hard-wire an emit call into CommitTransaction and log a copy-pasteable PITR recipe with the exact commit-record LSN, which no extension can observe. And kill v5's XactCallback signature change — that, not performance, is what committers will reject.

Why in-core changes the shape

The extension needed a PRE_COMMIT restore point because it couldn't trust the commit LSN (the XactLastCommitEnd stale-value trap). Core reads the LSN at the source — v5 already threads it out of RecordTransactionCommit. With an exact, correctly-chosen LSN, a restore point becomes pure overhead: extra WAL per DROP, name-collision semantics, and a second mechanism to document. The commit record is already in the archive; the log line is its index. Core also owns the recovery_target_* docs, so it can print the exact recipe and guarantee it stays correct.

Kirk's "it's not heavy" argument — steelmanned, then attacked

When off, the cost is literally one bool test in doDeletion (dependency.c:1613) and one in dropdb (dbcommands.c:1874) — per-drop, not per-tuple, not in any hot path. He is right that performance is a non-issue. But committers won't argue performance. They'll argue:

  1. The ABI break. v5 changes the RegisterXactCallback function-pointer signature to pass commit_lsn. That breaks pgaudit, sepgsql, and every extension with a xact callback — indefensible when core code can just call a function directly. This is the single most likely rejection trigger and it is trivially avoidable.
  2. Log-format-as-API and scope creep ("why not TRUNCATE?"). Answer: DROP is uniquely non-recoverable-in-place; draw that line explicitly in the docs.
  3. Reshke's "why not an extension" — needs the answer below.

What actually gets it committed: remove the callback ABI change, keep the diff under ~300 lines, and make the log line demonstrably correct (see #53 — a committer who tests the current recipe and replays the drop anyway will bounce the patch instantly).

The strongest in-core design

  • GUC log_object_drops, bool, PGC_SUSET, default off (as v5).
  • Registration where v5 puts it: doDeletion for RELKIND_RELATION/PARTITIONED_TABLE, non-temp, plus dropdb.
  • Emission: a direct AtEOXact_LoggedDrops(commit_start_lsn) call in CommitTransaction right after RecordTransactionCommit, with abort-side cleanup next to the other AtEOXact_* calls. No callback registration, no ABI change.
  • Output, per object: LOG: DROP TABLE: "s.t" (OID n), xid X, commit record at LSN A/B plus an errhint() giving the exact recovery recipe.
  • Against "the log is on the machine you just destroyed": DROP destroys data, not the server or its (typically shipped) logs; and the LSN merely locates a commit record that already lives in the archive — worst case pg_waldump re-derives it.

The pitch to hackers

When a table is dropped in production, the DBA's recovery time is dominated by one question: what LSN do I recover to? Today, answering it means pg_waldump archaeology under incident pressure. This patch answers it in one grep, using information only the commit path has — the commit record's LSN before it's discarded. No event trigger can do this: they don't fire for DROP DATABASE and can't see the commit LSN. It belongs in core exactly like log_checkpoints: operational breadcrumbs for the moment you need them most. Off by default, one branch in the drop path, zero cost until a DBA turns it on — and the first time it's used, it pays for itself in hours of downtime.


Where the two memos disagree

Memo 1 says ship it as an extension/contrib; Memo 2, told to assume core, says core genuinely buys something an extension cannot: the exact commit-record LSN, and ownership of the docs that make the recipe correct. These are consistent — the disagreement is entirely about whether that gain justifies core surface area, which is a judgement call for -hackers, not for an agent. What both agree on:

Refs


🤖 Generated with Claude Code

@NikolayS

Copy link
Copy Markdown
Owner Author

Fix tracking — Kirk's 4 items (was #56)

Tracking issue for the review items Kirk Wolak raised against v5 of "PoC: Simplify recovery after dropping a table by LOGGING the restore LSN" (log_object_drops GUC), posted verbatim to the Hacking Postgres call chat. Andrey Borodin confirmed on the call this is "a real list of problems in the patch".

Kirk's four items

  • 1. Format-string bug. errmsg format has 4 %X ("drop LSN: %X/%X, commit LSN: %X/%X") but passes one LSN_FORMAT_ARGS() = 2 args. Confirmed real UB, not cosmetic — clang: warning: more '%' conversions than data arguments [-Wformat-insufficient-args]. The last two %X read past the varargs. No "drop LSN" is recorded anywhere in the patch; message now reports the commit LSN only, in master's %X/%08X format.
  • 2. DROP DATABASE still uses GetXLogInsertRecPtr(). Kirill Reshke's issue, unfixed on that path. Read before the commit record exists, and dropdb() forces an immediate checkpoint before committing, so it could be far behind the real commit LSN. Now routed through the same deferred XactCallback path as tables.
  • 3. Skip TEMP. Not PITR-recoverable and dropped at every session exit → noise. v5's Test 9 asserted the opposite; inverted and expanded (explicit / ON COMMIT DROP / session exit).
  • 4. Document PITR use (recovery_target_inclusive). The trap: it defaults to on = stop after the target, so setting only recovery_target_lsn replays the drop and loses the table again. Documented with the required recovery_target_inclusive = off.

Additional findings

  • 5. v5 logs the wrong end of the commit record (found while writing the PITR test). XactLogCommitRecord() returns EndRecPtr, but recovery_target_lsn is compared against each record's start (recoveryStopsBefore() tests record->ReadRecPtr). The end LSN is the start of the next record, so even recovery_target_inclusive = off replays the drop — the documented recipe in item 4 does not work without this fix. Now reports ProcLastRecPtr. This one deserves list discussion: it changes the meaning of the LSN in the XactCallback API v5 introduces.
  • 6. v5 does not build on current master (explains the long-red CF CI):
    • guc_parameters.dat now enforces alphabetical ordering; log_object_drops was appended after zero_damaged_pages → hard build failure.
    • t/050_drop_table_logging.pl collides with master's 050_redo_segment_missing.pl → renumbered to 055.
  • 7. Testing strengthened (Andrey's ask). v5's tests largely asserted "a line exists", which cannot distinguish a correct LSN from garbage. Now 54 assertions incl. LSN pinned to the actual commit LSN (with ~7 MB of WAL written between DROP and COMMIT so drop LSN and commit LSN cannot be confused), GUC-off, ROLLBACK, ROLLBACK TO SAVEPOINT, TEMP, failed DROP DATABASE, and an end-to-end PITR round trip proving both directions of the recovery_target_inclusive trap.

Open questions for the list

  • Should unlogged tables be skipped as well? Contents are not PITR-recoverable either, though the definition is. Kirk's item said TEMP only, so scope was not expanded.
  • Is end-of-commit-record or start-of-commit-record the right thing for a general XactCallback LSN (item 5)?
  • v5's Assert(!XLogRecPtrIsInvalid(commit_lsn)) is retained — it holds, but fires on every commit.

Not done

  • Not posted to pgsql-hackers / not attached to CF #6272 — GitHub PR only so far.
  • Docs not rendered (DocBook DTD unavailable offline locally); pgindent not run (pg_bsd_indent not installed); verified on macOS/arm64 + meson + cassert only. See the PR's "What I could NOT verify" section.

@NikolayS

Copy link
Copy Markdown
Owner Author

Session summary 2026-07-15 (was #57)

Summary of the Hacking Postgres session of 2026-07-15 (Nikolay Samokhvalov, Kirk Wolak, Andrey Borodin), which worked on the stalled log_object_drops patch.

The patch

"PoC: Simplify recovery after dropping a table by LOGGING the restore LSN" — logs the commit LSN when a DROP TABLE / DROP DATABASE commits, so a DBA who dropped something by accident knows the PITR target.

Outputs from this session

PR #55 — the fix #55
#56 — fix tracking issue #56
#53 — the LSN correctness bug #53
#54 — design review (two memos) #54
Branch fix/drop-lsn-v5-fixes, commit 15aa5609c65, off origin/master @ 11ed011ae22

What was found

1. The feature did not work at all (#53)

v5 logged the end LSN of the commit record (XactLogCommitRecord() returns XLogInsert's EndPos). Recovery compares recovery_target_lsn against each record's start (recoveryStopsBefore() tests record->ReadRecPtr, xlogrecovery.c:2580-2582).

Consequence: targeting the logged LSN never stops before the drop, with any value of recovery_target_inclusive — recovery replays the commit and stops before the next record. The object is lost again. As Andrey put it on the call: the logged LSN is one record past the actual commit of the drop. And the DBA cannot correct it by hand, because the record length is not recoverable from the log line.

Fixed in #55: logs ProcLastRecPtr (xlog.h:33), which XLogInsertRecord() sets to the start of the record just inserted (xlog.c:1112). Confirmed with pg_waldump that the logged LSN lands exactly on the drop transaction's COMMIT record. Same idiom CreateCheckPoint already relies on (xlog.c:7772).

This was found independently by two agents, from different directions.

2. v5 does not build on current master — explains 7 months of red CI

Two independent breaks, both fixed in #55:

  • guc_parameters.dat now enforces alphabetical ordering; log_object_drops was appended after zero_damaged_pages → hard build failure.
  • t/050_drop_table_logging.pl collides with master's existing 050_redo_segment_missing.pl → renumbered to 055.

Nobody could have reviewed this patch even if they had wanted to.

3. Kirk's four defects — all fixed in #55

  1. Format-string bug — errmsg had 4× %X but one LSN_FORMAT_ARGS (expands to 2 args). Confirmed real UB, not cosmetic; reproduced warning: more '%' conversions than data arguments.
  2. DROP DATABASE still used GetXLogInsertRecPtr()dropdb() forces a checkpoint before committing, so the old value could be far behind the commit LSN. Now routed through the same deferred path.
  3. TEMP skipped — v5's Test 9 asserted the opposite; inverted and expanded.
  4. PITR docsrecovery_target_inclusive = off documented as required.

4. Open question raised on the call: unlogged tables

The patch filters RELPERSISTENCE_TEMP only, so unlogged tables are still logged. PITR recovers an unlogged table's definition but zero rows — the catalog entry is WAL-logged, the data is not. The log line therefore promises a recovery that returns an empty table. Suggest filtering != RELPERSISTENCE_PERMANENT.

Design discussion (#54)

Two memos: one with the extension option open, one with it explicitly off the table (per Andrey and Kirk).

  • Restore points vs. log lines. Kirk's "fake LSN before the commit" is precisely a restore point. Concluded unnecessary: ProcLastRecPtr gives the exact commit-record start for free, no WAL, no name. A restore point costs WAL on every DROP (including ones that then ROLLBACK), needs wal_level >= replica, needs a unique name, and recovery_target_name stops after the first name match. Its one advantage: it lives in WAL, so it survives losing the logs.
  • Why not an extension? Kirill Reshke asked exactly this on the thread in March 2025 — "What stops us from logging all the same inside object access hook defined by extension?" — and was never answered. Nik's userspace version (a sql_drop event trigger calling pg_create_restore_point()) works today for DROP TABLE. The honest answers: it does not work for DROP DATABASE (event triggers are per-database), it cannot see the commit LSN, and it is opt-in per database so protects nobody by default. The thread needs an answer to Reshke ready.
  • XactCallback ABI. v5 mutates the public typedef in place (xact.h:139). Andrey correctly noted this is not a blocker — Postgres breaks ABI between major releases routinely. What remains is an API-churn argument: every xact-callback author must edit their signature to benefit one feature, when core can just call the function directly.

Review feedback still to apply

From Andrey, on the call:

  • Commit message is not Postgres style — needs 2-3 paragraphs on why and what changed, not our process, plus a Discussion: link.
  • The ProcLastRecPtr comment should be shorter, and should assert the invariant (that the pointer is the actual commit record, and is never invalid inside RecordTransactionCommit) rather than narrate the problem.
  • Attribution needs fixing — the commit is currently authored solely as Nikolay Samokhvalov, with no Author: for Dmitry Lebedev, whose patch this mostly is.
  • CI is now enabled on this fork (PG_CI_ENABLED=1), so the next push runs the real matrix — Linux, Windows, 32-bit, macOS.

Not verified

Stated plainly by the agent that produced #55:

  • Docs do not build locally (DocBook DTD fetch fails); the new <varlistentry> is well-formed and both xref targets exist, but rendered output is unverified.
  • pgindent not run (pg_bsd_indent not installed).
  • macOS/arm64 only — no Linux, no 32-bit, no autoconf, no Windows. (CI now closes this.)
  • sepgsql not compiled; its XactCallback signature change is inspection-only.
  • LSN-exactness assertions assume a quiet WAL stream — the most plausible buildfarm flakiness risk.
  • Nothing has been posted to pgsql-hackers or attached to CF #6272.

Next steps

  1. Rewrite the commit message in Postgres style; tighten the comment; fix attribution.
  2. Decide on unlogged tables.
  3. Send v6 to pgsql-hackers with a plain-text email, bump the thread, move CF #6272 to an open commitfest (PG20-1).
  4. Have an answer ready for Reshke's question.

🤖 Generated with Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant